home *** CD-ROM | disk | FTP | other *** search
/ Floppyshop 2 / Floppyshop - 2.zip / Floppyshop - 2.iso / diskmags / 5791-.end / dmg-6143 / lza_rept / compres4.txt < prev    next >
Text File  |  1996-06-15  |  12KB  |  241 lines

  1.                Experiments in Data Compression IV
  2.                     (or "The Need for Speed")
  3.                          by John Kormylo
  4.                          E.L.F. Software
  5.  
  6.  
  7. Binary Search Tree
  8.  
  9.      The  LZA  algorithm  uses a binary  search  tree  to  locate 
  10. strings.   Specifically it consists of an array of structures  of 
  11. the form
  12.  
  13. typedef struct tree {
  14.   struct tree *parent;  /* binary tree pointers, or NULL */
  15.   struct tree *child1;  /* less than */
  16.   struct tree *child2;  /* greater than */
  17.   unsigned char *text;  /* string pointer */
  18.   unsigned int count;   /* number of children + 1 */
  19.   int first;            /* number of bytes common with parent */
  20.  } TREE;
  21.  
  22. with  one structure for every string in the  dictionary.   (Note, 
  23. these  are not NULL terminated strings,  but it is easier to  say 
  24. "string"  than "block of 65 consecutive bytes.")  One would do  a 
  25. string  compare  to 'text' and move to 'child1' or  'child2'  and 
  26. repeat.   The  'parent' entry was used for  tree  balancing,  but 
  27. turns out not to be necessary.   The 'count' entry was used  both 
  28. for tree balancing and for coding,  and the 'first' entry was  an 
  29. attempt  to  reduce  the number of byte  comparisons  during  the 
  30. string compares.  (However, nothing guarentees that child strings 
  31. will have ANY bytes in common.)
  32.      To locate the longest match one must search until you hit  a 
  33. NULL  pointer.   Given  the first (in search order) match  for  a 
  34. given  length,  one must search all child strings to  locate  the 
  35. most  recent matching string (for the FIFO buffer) or search  for 
  36. the lowest and highest matching strings (for the dictionary).
  37.  
  38.  
  39. FIFO Buffer - Hash Table
  40.  
  41.      Actually,  the method developed in the last report was  more 
  42. of  a  cross  between  a  hash  table  and  binary  search  tree.  
  43. Specifically, it consisted of 16 byte structures of the form:
  44.  
  45. typedef struct hash {
  46.   struct hash *child1;    /* less than */
  47.   struct hash *child2;    /* greater than */
  48.   struct hash *next;      /* start of next tree */
  49.   unsigned int sum;       /* sum of all matching strings */
  50.   unsigned char index;    /* index of most recent match */
  51.   unsigned char test;     /* character */
  52.  } HASH;
  53.  
  54. Given a pointer to the start of the table,  one would search  for 
  55. the  first  byte in the string using a binary search  tree  using 
  56. 'child1',  'child2',  and 'test'.   Once you have found the first 
  57. byte,  you can search for the second byte using the binary search 
  58. tree which starts at 'next'.  'Sum' is used by the tree balancing 
  59. algorithm,  and 'index' is the array index for the  corresponding 
  60. string in the FIFO buffer.
  61.      The  number of structures used is variable,  with the  worst 
  62. case being 64 * 65 * sizeof(HASH) = 65K bytes, since there are 64 
  63. strings in the FIFO buffer, and each string has at most 65 bytes.  
  64. The  structures are initialized as a simple linked list  of  free 
  65. structures.   Also,  you will need an array of 64 string pointers 
  66. and weights.   (The hash table is used for coding only.)
  67.      The  advantage  of this approach is that  we  have  replaced 
  68. string  compares  with byte compares.   Once the  first  byte  is 
  69. found, one need only search for the second bytes, etc.  Also, the 
  70. 'index' value is automatically reset each time a string is  added 
  71. to  the hash table,  so one need not search for the  most  recent 
  72. match.
  73.  
  74. Main Dictionary - Layered Binary Search Tree
  75.  
  76.      This  approach combines the speed of a hash table  with  the 
  77. low  memory requirement of a binary search tree.   As before  you 
  78. need  an  array  of  structures,  one  for  each  string  in  the 
  79. dictionary.   In  addition  you will need a  variable  number  of 
  80. structures  which point to the same strings,  but are located  in 
  81. lower layers.   The worst case is also the same as the number  of 
  82. strings  in the dictionary,  for a total of twice the RAM  needed 
  83. for a simple binary search tree.
  84.      The structures used here are given by:
  85.  
  86. typedef struct search {
  87.   struct search *child1;  /* less than */
  88.   struct search *child2;  /* greater than */
  89.   struct search *next;    /* start of next tree */
  90.   unsigned char *text;    /* string pointer */
  91.   unsigned int sum;       /* sum of all children */
  92.   unsigned char len;      /* number of common chars */
  93.   unsigned char test;     /* first uncommon char */
  94.  } NODE;
  95.  
  96. The 'len' parameter is constant over a layer.   The 'test'  entry 
  97. is redundant,  where p->test = p->text[p->len],  but is faster to 
  98. access  and is free (since structures always take an even  number 
  99. of bytes).
  100.      Given a pointer to the start of the tree,  one would compare 
  101. the first byte against 'test' and perform the usual binary search 
  102. via 'child1' and 'child2'.  If a match is found, 'next' points to 
  103. the start of another binary search tree,  but not necessarily for 
  104. the second byte.   One would first have to check all the bytes in 
  105. p->text[1]  through  p->text[p->len - 1] (where p points  to  the 
  106. first entry in the next layer).   If they all match, one can then 
  107. search  for  a  matching 'test' using the binary  tree  for  this 
  108. layer, etc.
  109.      The  advantage is again that you need only search  for  each 
  110. byte once.   Also,  when you find a match of a given length,  the 
  111. number  of matching entries is given either by 'sum' (if you  are 
  112. between layers), 'next->sum' (if it exists), or is 1.
  113.      The trick is to copy the parent structure (at least, it uses 
  114. the  same 'text' pointer) whenever you start an new  layer,  then 
  115. add the new string as a child structure of the copy.   Similarly, 
  116. whenever  you remove a string from the dictionary,  you  have  to 
  117. search the next layer to find the duplicate,  replace the  parent 
  118. with an structure from the next layer,  and replace the duplicate 
  119. with  a  copy of the new parent (unless there are no more entries 
  120. on that layer).  It should be remembered that the duplicate to be 
  121. removed might also have a duplicate in the next layer.
  122.  
  123. Tree Balancing
  124.  
  125.      Consider the two binary search trees organised as follows:
  126.  
  127.                    A                   C
  128.                     \                 /
  129.                      \               /
  130.                       C             A
  131.                      /               \
  132.                     B                 B
  133.  
  134. where A < B < C by value.  It should be noted that any additional 
  135. children of A B or C would remain in place if one were to convert 
  136. from one to the other.  This transformation is called a rotation.
  137.      Starting  with A using the left tree,  suppose we wanted  to 
  138. add a new node D > C.   Starting at A we see that we need to move 
  139. to right.  However, before doing so we check to see if the number 
  140. of children to the right (child2->sum) is greater than the number 
  141. of  children to the left (child1->sum).   If so,  we perform  the 
  142. rotation.   Since we are now at C instead of A, we need to repeat 
  143. the comparison.  Also, before moving down we should increment the 
  144. 'sum' entry at C.
  145.      One  can  avoid the need for a parent pointer by  using  the 
  146. approach:
  147.  
  148. HASH *p,**from;
  149.  
  150.   from = &start;
  151.   p = *from;
  152.   while(p) {
  153.     ...
  154.     if(byte < p->test) from = &(p->child1);
  155.     else if(byte > p->test) from = &(p->child2);
  156.     else from = &(p->next);
  157.     p = *from;
  158.    }
  159.  
  160. Changing '*from' will change the appropriate pointer,  no  matter 
  161. where it came from.
  162.  
  163.  
  164. Fine Tuning
  165.  
  166.      The following table shows the results for various methods of 
  167. choosing  which  matches to use.   The first number is  the  time 
  168. required  to perform the compression (accurate to 2 seconds)  and 
  169. the second is the size of the file.   The 'ZIP' entry is used for 
  170. comparison (times are not available).
  171.  
  172.  
  173.        | DEMONSTR.DOC | ELFBACK.PRG | WORDLIST.TXT |
  174. -------+--------------+-------------+--------------+
  175.    ZIP |       25134  |      32417  |      496876  |
  176.  Smart |  18   22284  |  22  32567  | 418  503524  |
  177.   Best | 102   22029  |  64  31997  |1670  453142  |
  178.  Best1 |  76   21746  |  54  32451  |1654  446201  |
  179.  Best2 |  70   21960  |  50  32372  |1454  446607  |
  180. -------+--------------+-------------+--------------+
  181.     8K |  20   22444  |  22  32405  | 424  496712  |
  182.    384 |  60   21979  |  40  32417  |1190  419201  |
  183.  8K384 |  60   22133  |  40  32288  |1164  421197  |
  184. -------+--------------+-------------+--------------+
  185.  
  186.      The 'Smart' method is essentially the same as that developed 
  187. in the previous report.   It looks for the longest combination of 
  188. two strings.   To decide between a string from the FIFO buffer or 
  189. the main dictionary,  each match is ranked by the weight for that 
  190. offset times the total number of entries in the main  dictionary, 
  191. or the sum of matches in the main dictionary times the sum of all 
  192. the  weights  for  the  FIFO  buffer.    To  decide  between  two 
  193. combinations of strings which have the same combined  length,  it 
  194. looks at the rank times the length of the first match.   This  is 
  195. the fastest method which is still competitive with ZIP.
  196.      The 'Best' method looks at all possible ways to compress the 
  197. next 65 bytes until a unique first step is determined.   Analysis 
  198. by  the  Pure  Profiler showed that most of  time  is  now  spent 
  199. computing the number of bits needed to code strings,  rather than 
  200. searching for them.
  201.      'Best1' shows a variation of the 'Best' algorithm which only 
  202. codes a single byte using arithmetic if there is no other way  to 
  203. get to the next byte.   'Best2' also checks if the number of bits 
  204. used to get to the end of the longest combination found so far is 
  205. less  than the number of bits needed to get to the current  byte, 
  206. in  which case it doesn't bother to test if any strings  starting 
  207. from  the current byte will help.   While these  variations  only 
  208. achieve marginal improvements in speed, there is very little loss 
  209. of  compression,  and sometimes they even out-perform the  'Best' 
  210. algorithm.
  211.      The  best  thing about all these methods is  that  they  are 
  212. totally  compatible  with  the current  versions  of  ELFARC  and 
  213. ELFBACK.   However,  if  we relax  this  requirement,  additional 
  214. savings are possible.
  215.      '8K'  shows  the  'Smart' algorithm results  when  the  main 
  216. dictionary  is  reduced  from 16K to 8K  entries  (640K  to  320K 
  217. bytes).  Note that this method beats ZIP on all three files.
  218.      '384' is a variation of 'Best2' which uses 384 codes instead 
  219. of 320.   Specifically,  these consist of the 256 basic codes, 64 
  220. length codes for the FIFO buffer and 64 length codes for the main 
  221. dictionary.   This  was  intended to reduce the number  of  steps 
  222. needed to code a string (or compute the number of bits needed  to 
  223. code  a string).   Whether it benefits of hurts  the  compression 
  224. depends  on whether the distribution of lengths are  similar  for 
  225. the  two dictionaries.  '8K384' uses both an 8K  main  dictionary 
  226. and the 384 codes.   Interestingly,  using 384 codes had  benefit 
  227. for the 'Smart' algorithm whatsoever.
  228.      I also discovered that a very minor improvement in speed  is 
  229. possible  by sorting the codes in the arithmetic  compression  so 
  230. that the most common codes are close together.
  231.  
  232. Conclusion
  233.  
  234.      At  this  point  the difference  in  compression  efficiency 
  235. between  Smart  and  Best approaches is  minor  compared  to  the 
  236. difference in speed.   Since the '8K' method satisfies the design 
  237. criteria,  it will be used for the next versions of both  ELFBACK 
  238. and ELFARC.
  239.  
  240.  
  241.